﻿/*
Copyright (c) 2010, Luca Deltodesco
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions
	  and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of
	  conditions and the following disclaimer in the documentation and/or other materials provided
	  with the distribution.
    * Neither the name of the caxe project nor the names of its contributors may be used to endorse
	 or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package ;

using SUse;
using SUtil;
using FUtil;

import Types;
import SUse;

enum ScopeType {
    sGlobal;
    sFile;
    sObject;
    sMethod;
    sLocal;
}

class Proc {
    public static inline function equal<T>(a:T, b:T) {
        return switch(a) {
            case b: true;
            default: false;
        }
    }

    public static function proc_macro(scope:Scope, src:String) {
        var ret = null;
        
        var err1 = "End of macro definition unexpected\n";
        if(src.length<5) {
            CaxeInfo.errors.push("UNKNOWN MACRO TYPE "+src+"\n"+scope.actfile.name+".cx");
            return null;
        }
        
        var pos = Integer.get(0);
        src.skipall(pos);
        if(pos.i>=src.length) { CaxeInfo.errors.push(err1); return null; }
        
        var npos = pos.i;
        src.skipword(pos);
        var mtype = src.substr(npos,pos.i-npos);
        
        var type = if(mtype=="mixin")  mMixin
              else if(mtype=="define") mDefine
              else if(mtype=="expand") mExpand
              else null;
        if(type==null) {
            CaxeInfo.errors.push("UNKNOWN MACRO TYPE "+mtype+"\n"+scope.actfile.name);
            return null;
        }else ret = new Macro(type);
        
        var access = null,defacc = null;
        
        defacc = if(scope==scope.file)   sFile
            else if(scope==scope.object) sObject;
            else if(scope==scope.method) sMethod;
            else                         sLocal;
            
        //-----------
        
        while(true) {
            src.skipall(pos);
            if(pos.i>=src.length) { CaxeInfo.errors.push(err1); return null; }
            
            npos = pos.i;
            src.skipword(pos);
            var mod = src.substr(npos,pos.i-npos);
            if(mod=="newfile") {
                if(equal(type,mDefine))
                    ret.newfile = true;
                continue;
            }

            access = if(mod == "global" ) sGlobal
                else if(mod == "local"  ) sLocal
                else if(mod == "file"   ) sFile
                else if(mod == "object" ) sObject
                else if(mod == "method" ) sMethod
                else {
                    pos.i = npos;
                    defacc;
                };
            
            break;
        }
        
        if((equal(access,sObject) || equal(access,sMethod)) && equal(defacc,sFile)) {
            CaxeInfo.errors.push("IMPOSSIBLE MACRO SCOPE\n"); return null;
        }else if(equal(access,sMethod) && equal(defacc,sObject)) {
            CaxeInfo.errors.push("IMPOSSIBLE MACRO SCOPE2\n"); return null;
        }else if(equal(access,sGlobal) && scope.file==null) {
            CaxeInfo.errors.push("IMPOSSIBLE MACRO SCOEP3\n"); return null;
        }
        
        src.skipall(pos);
        if(pos.i>=src.length) { CaxeInfo.errors.push(err1); return null; }
        
        npos = pos.i;
        if(equal(type,mDefine)) {
            var ipos = pos.i;
            if(!src.skipmac(pos)) {
                pos.i = npos;
                src.skipword(pos);
                ret.name = src.substr(npos,pos.i-npos);
            }else {
                var mpos = pos.i;
                src.rskipws(pos);
                var epos = pos.i;
                src.rskipword(pos);
                ret.preamble = src.substr(ipos,pos.i-ipos);
                ret.name = src.substr(pos.i,epos-pos.i);
                pos.i = mpos;
            }
        }else {
            src.skipword(pos);
            ret.name = src.substr(npos,pos.i-npos);
        }
        
        if(pos.i>=src.length) { CaxeInfo.errors.push(err1); return null; }
        
        var args = new Array<String>();
        src.skipall(pos);
        if(pos.i>=src.length) { CaxeInfo.errors.push(err1); return null; }
        
        if(src.iat(pos)=='('._c()) {
            ret.argc = src.getargs(pos,args);
            pos.i++;
        }else ret.argc = 0;
        
        for(it in 0...args.length) {
            var i = args[it];
            if(!i.isword(0,i.length)) {
                CaxeInfo.errors.push("INVALID ARGUMENT NAME '"+i+"'\n");
                return null;
            }
            for(jt in (it+1)...args.length) {
                if(args[jt] == i) {
                    CaxeInfo.errors.push("[WARNING] DUPLICATE ARGUMENT NAME '" + i + "'\n");
                }        
            }
        }
        
        while(pos.i<src.length) {
            var argn = 0, argl = 0;
            var ind = SUtil.npos; var carg = 0;
            for(arg in args) {
                var cargl = arg.length;
                var argi = Integer.get(pos.i);
                while(argi.i<src.length && (argi.i<ind || ind==SUtil.npos)) {
                    argi.i = src.find_first(arg,argi.i);
                    if(argi.i==SUtil.npos) break;
                    if(src.isword(argi.i,cargl)) break;
                    argi.i++;
                }
                if((argi.i<ind || ind==SUtil.npos) && argi.i!=SUtil.npos) {
                    ind = argi.i;
                    argn = carg;
                    argl = cargl;
                }
                carg++;
            }
            
            if(ind==SUtil.npos) {
                var nent = new Post(true);
                nent.str = src.substr(pos.i);
                ret.postamble.push(nent);
                
                break;
            }else {
                var precom = ind>0 && src.cat(ind-1)=='`'._c();
                var poscom = ind+argl<src.length && src.cat(ind+argl)=='`'._c();
                
                var nent = new Post(true);
                nent.str = if(precom) src.substr(pos.i,ind-1-pos.i);
                           else       src.substr(pos.i,ind-pos.i);
                ret.postamble.push(nent);
                
                nent = new Post(false);
                nent.argn = argn;
                ret.postamble.push(nent);
                
                pos.i = ind+argl;
                if(poscom) pos.i++;
            }
        }
        
        ret.scope = scope;
        
        var macros = if(equal(access,sLocal )) scope.locals
                else if(equal(access,sGlobal)) scope.actfile.locals
                else if(equal(access,sFile  )) scope.file.locals
                else if(equal(access,sObject)) scope.object.locals
                else                           scope.method.locals;
        
        for(i in macros) {
            if(i.equal(ret)) {
                CaxeInfo.errors.push("MACRO ALREADY EXISTS IN SCOPE '"+ret.name+"'\n");
                return null;
            }
        }
        macros.push(ret);
        
        return ret;
    }
    
    //----------------------------------------------------------------------------------------------
    
    public static function proc(scope:Scope) {
        var ret = false;
        
        var c0 = 0, c1 = 0, c2 = 0;
        
        var src = null;
        var setsrc = function(x) { return x; }
        var id = 0;
        while(id<scope.data.length) {
            var i = scope.data[id];
            
            if(!equal(i.type, dString)) { id++; continue; }
            
            src = i.str;
            setsrc = function(x) { return i.str = x; };
            if(src.find_first_not_any(SUtil.ws_str)==SUtil.npos) {
                scope.data.splice(id,1);
                continue;
            }
            
            var pos = Integer.get(0);
            while(pos.i<src.length) {
                if(src.skipall(pos)) continue;
                
                var ch = src.iat(pos);
                if     (ch=='('._c()) c0++; else if(ch==')'._c()) c0--;
                else if(ch=='{'._c()) c1++; else if(ch=='}'._c()) c1--;
                else if (ch == '['._c()) c2++; else if (ch == ']'._c()) c2--;
                
                if(ch=='{'._c() && c0==0 && c2==0) {
                    var ppos = pos.i+1;
                    src.skiprbrace(pos);
                    
                    var ndat = new Datum(dScope);
                    var nsc = new Scope();
                    ndat.scope = nsc;
                    var ndat2 = new Datum(dString);
                    ndat2.str = src.substr(ppos,pos.i-ppos);
                    ndat.scope.data.push(ndat2);
                    
                    scope.data.insert(id+1,ndat); id++;
                    
                    if(scope.object==null)
                        scope.object = nsc;
                    else if(scope.method==null)
                        scope.method = nsc;
                    
                    nsc.file = scope.file;
                    nsc.object = scope.object;
                    nsc.actfile = scope.actfile;
                    nsc.method = scope.method;
                    nsc.parent = scope;
                    proc(nsc);
                    
                    ndat = new Datum(dString);
                    ndat.str = src.substr(pos.i+1);
                    
                    scope.data.insert(id+1,ndat); id++;
                    
                    setsrc(src.substr(0,ppos-1));
                    src = ndat.str;
                    setsrc = function(x) { return ndat.str = x; }
                    pos.i = 0;
                    continue;
                }
                
                else if(ch=='$'._c() && c0==0 && c2==0) {
                    var ppos = pos.i+2;
                    src.skipnmac(pos);
                    
                    var macstr = src.substr(ppos,pos.i-ppos);
                    var mac = proc_macro(scope,macstr);
                    
                    if(pos.i+1<src.length && src.cat(pos.i+1)==';'._c()) pos.i++;
                    var prestr = src.substr(0,ppos-2);
                    var posstr = src.substr(pos.i+1);
                    if(mac==null) {
                        src = setsrc(prestr+posstr);
                        pos.i = ppos-2;
                        continue;
                    }else {
                        var ndat = new Datum(dMacro);
                        ndat.macro = mac;
                        scope.data.insert(id+1,ndat); id++;
                        
                        ndat = new Datum(dString);
                        ndat.str = posstr;
                        scope.data.insert(id+1,ndat); id++;
                        
                        setsrc(prestr);
                        src = ndat.str;
                        setsrc = function(x) { return ndat.str = x; }
                        pos.i = 0;
                        
                        ret = true;
                        continue;
                    }
                }
                
                pos.i++;
            }
            
            id++;
        }
        
        return ret;
    }
    
    //----------------------------------------------------------------------------------------------
    
    public static function step(scope:Scope) {
        var ret = false;
        var macros = scope.query();
        
        var id = 0;
        while(id<scope.data.length) {
            var csd = scope.data[id];
            if(equal(csd.type,dMacro)) { id++; continue; }
            
            if(equal(csd.type,dScope)) {
                while(step(csd.scope)) {}
                id++;
                continue;
            }
            
            var str = csd.str;
            var setstr = function(x) { return csd.str = x; };
            var pos = Integer.get(0);
            while(pos.i<str.length) {
                if(!SUtil.charch(str.iat(pos))) { pos.i++; continue; }
                
                var ipos = pos.i;
                str.skipword(pos);
                var mname = str.substr(ipos,pos.i-ipos);

                for(cmac in macros) {
                    if(cmac.name == mname) {
                        var ppos = pos.i;
                        var args = new Array<String>();
                        
                        if(cmac.argc!=0) {
                            str.skipws(pos);
                            if(pos.i>=str.length) { pos.i = ppos; continue; }
                            if(str.iat(pos)!='('._c()) { pos.i = ppos; continue; }
                            
                            var argc = str.getargs(pos,args);
                            if(argc!=cmac.argc) { pos.i = ppos; continue; }
                            pos.i++;
                        }else {
                            var tpos = Integer.get(pos.i);
                            str.skipws(tpos);
                            if(tpos.i<str.length) {
                                if(str.iat(tpos)=='('._c()) {
                                    tpos.i++;
                                    str.skipws(tpos);
                                    if(tpos.i<str.length) {
                                        if(str.iat(tpos)==')'._c()) {
                                            pos.i = tpos.i+1;
                                        }
                                    }
                                }
                            }
                        }
                        
                        var earlyexit = false;
                        for(carg in args) {
                            var apos = Integer.get(0);
                            while(apos.i<carg.length) {
                                if(!SUtil.charch(carg.iat(apos))) { apos.i++; continue; }
                                
                                var iapos = apos.i;
                                carg.skipword(apos);
                                var aname = carg.substr(iapos,apos.i-iapos);
                                
                                for(amac in macros) {
                                    if(amac.name == aname) {
                                        var papos = apos.i;
                                        if(amac.argc!=0) {
                                            carg.skipws(apos);
                                            if(apos.i>=carg.length) { apos.i = papos; continue; }
                                            if(carg.iat(apos)!='('._c()) { apos.i = papos; continue; }
                                            
                                            var temp = new Array<String>();
                                            var argc = carg.getargs(apos,temp);
                                            if(argc!=amac.argc) { apos.i = papos; continue; }
                                        }
                                        ret = true;
                                        earlyexit = true;
                                        break;
                                    }
                                }
                                if(earlyexit) break;
                            }
                            if(earlyexit) break;
                        }
                        
                        if(earlyexit && !equal(cmac.type,mMixin)) { pos.i = ppos; continue; }
                        
                        var mscope = null;
                        var found = false;
                        var macname = "";
                        if(!equal(cmac.type,mMixin)) {
                            macname = mname+"_"+args.textify();
                            for(k in cmac.instances) {
                                if(k.mname == macname) {
                                    mscope = k.compilation;
                                    found = true;
                                    break;
                                }
                            }
                        }
                        if(!found) {
                            mscope = new Scope();
                            if(!equal(cmac.type,mMixin)) {
                                var inst = new Instance();
                                inst.mname = macname;
                                inst.compilation = mscope;
                                cmac.instances.push(inst);
                            }
                            
                            if(equal(cmac.type,mMixin)) {
                                mscope.actfile = scope.actfile;
                                mscope.parent = scope;
                            }else {
                                mscope.actfile = cmac.scope.actfile;
                                mscope.parent = cmac.scope;
                            }
                            var succcont = false;
                            if(equal(cmac.type,mDefine)) {
                                if(cmac.newfile) {
                                    var cpath = cmac.scope.actfile.tpath;
                                    var ret = new File();
                                    ret.name = cmac.name+"_"+args.textify();
                                    ret.tpath = cpath.directory()+ret.name+"."+cpath.extension();
                                    
                                    ret.imports = cmac.scope.actfile.imports.copy();
                                    ret.locals  = cmac.scope.actfile.locals.copy();
                                    ret.importmacs = cmac.scope.actfile.importmacs.copy();
                                    
                                    var nsc = new Scope();
                                    nsc.actfile = ret;
                                    //nsc.locals = cmac.scope.locals.copy();
                                    ret.data = nsc;
                                    nsc.file = nsc;
                                    nsc.parent = nsc.method = nsc.object = null;
                                    
                                    var nent = new Datum(dString);
                                    nsc.data.push(nent);
                                    nent.str = cmac.compile(macname,args);
                                    ret.packimpo = cmac.scope.actfile.packimpo.copy();
                                    ret.packname = cmac.scope.actfile.packname;
                                    
                                    mscope = nsc;
                                    proc(mscope);

                                    if(mscope.locals.length!=0) {
                                        CaxeInfo.errors.push("MACROS CANNOT HAVE OUTWITH SCOPE\n");
                                        pos.i = ppos; continue;
                                    }
                                    while(step(mscope)) {}
                                    
                                    CaxeInfo.files.set(ret.tpath.filename(),ret);
                                    CaxeInfo.efiles.push(ret);
                                    
                                    var pr = function (cefiles:Array<File>) {
                                        for(cf in cefiles) {
                                            var found = false;
                                            for(j in cf.importmacs) {
                                                if(j == ret.packname || j.length==0) {
                                                    found = true;
                                                    break;
                                                }
                                            }
                                            if(found) {
                                                found = false;
                                                for(j in cf.packimpo) {
                                                    if(j == ret.packname+"."+ret.name) {
                                                        found = true;
                                                        break;
                                                    }
                                                }
                                                if(!found) {
                                                    cf.packimpo.push(ret.packname+"."+ret.name);
                                                    if(CaxeInfo.files.exists(ret.name)) {
                                                        cf.imports.push(CaxeInfo.files.get(ret.name));
                                                    }
                                                }
                                            }
                                        }
                                    };
                                    pr(CaxeInfo.cfiles);
                                    pr(CaxeInfo.efiles);
                                    
                                    succcont = true;
                                }
                            }
                            if(!succcont) {
                                var nent = new Datum(dString);
                                nent.str = cmac.compile(macname,args);
                                
                                mscope.data.push(nent);
                                proc(mscope);
                                if(mscope.locals.length!=0) {
                                    CaxeInfo.errors.push("MACROS CANNOT HAVE OUTWITH SCOPE\n");
                                    pos.i = ppos; continue;
                                }
                                while(step(mscope)) {}
                            }
                        }
                        
                        if(equal(cmac.type,mDefine)) {
                            if(cmac.newfile) {
                                var ex = scope.actfile.extras;
                                var found = false;
                                var nval = "import ";
                                if(cmac.scope.actfile.packname.length>0) nval+=cmac.scope.actfile.packname+".";
                                nval += macname+";\n";
                                
                                for(i in ex) {
                                    if(i==nval) {
                                        found = true;
                                        break;
                                    }
                                }
                                if(!found) ex.push(nval);
                            }
                        }
                        
                        var posstr = str.substr(pos.i);
                        str = setstr(str.substr(0,ipos));
                        if     (equal(cmac.type,mMixin )) str = setstr(str+mscope.build());
                        else if(equal(cmac.type,mDefine)) str = setstr(str+macname);
                        
                        pos.i = str.length;
                        str = setstr(str+posstr);
                        
                        continue;
                    }
                }
                pos.i++;
            }
            id++;
        }
        return ret;
    }
}

